# Slice 2b-C fail-fast probe: launches the caller's program, waits up to HP_FAILFAST_PROBE_MS to
# classify it as "exited fast" (stale/broken cached artifact -- discard+rebuild candidate) vs.
# "still running" (the user's real, possibly long-running program -- never touched again). Never
# calls $p.Kill() -- past the probe window the wait is unbounded so a healthy app is never
# force-stopped.
#
# Inputs via env vars (avoids cmd.exe quoting hazards): HP_PROBE_EXE, HP_PROBE_ARGS, HP_PROBE_CWD,
# HP_FAILFAST_PROBE_MS, HP_PROBE_OUT/HP_PROBE_ERR (default ~run.out.txt/~run.err.txt),
# HP_PROBE_RESULT (default ~probe_result.txt -- "$exceeded|$exitcode", NOT stdout). Caller must
# pre-truncate output/result files before invoking.
#
# derived requirement ([REQ-026] argv passthrough): HP_PROBE_ARGS is now a full, already-quoted
# Arguments string (e.g. `"entry.py" "--foo" "bar"`), used verbatim -- not a single bare path
# re-quoted here. Caller quotes each token; see run_setup.bat's HP_APP_ARGS.
#
# Live-tees the child's stdout/stderr so a stdin-interactive program's prompts reach a real
# double-clicked user, instead of only writing captured output to disk at exit.
#
# derived requirement: does NOT use Register-ObjectEvent on OutputDataReceived/ErrorDataReceived.
# That was the original design and was found, via this repo's own local pwsh testing, to
# reorder lines WITHIN a single stream (e.g. round 2's output landing before round 1's in the
# captured/teed text, non-deterministically) -- root-caused to a confirmed, filed PowerShell bug
# (PowerShell/PowerShell#11937): those events dispatch via ThreadPool.QueueUserWorkItem, which
# does not guarantee delivery order when several lines arrive close together. Fixed by polling
# reads directly instead: only ONE read is ever in flight per stream at a time (the next read is
# not issued until the current one is consumed), so there is no possible out-of-order delivery
# for a single stream -- ordering is self-sequenced, not dependent on any runtime's
# callback-scheduling guarantee. Cross-stream (stdout vs stderr) interleaving was never
# guaranteed and still isn't -- that reflects the child's own two independent pipes, not a bug.
#
# derived requirement (Finding 9, 2026-07-24): reads via StreamReader.ReadAsync(char[], int, int)
# (raw chunks), NOT ReadLineAsync(). ReadLineAsync() only returns once it sees a full
# newline-terminated line -- confirmed empirically that Python's own `input("prompt")` (no
# trailing newline by design, so the cursor stays on the same line) is genuinely flushed to the
# OS pipe immediately but stays invisible to a ReadLineAsync-based reader until something else
# later flushes a newline, or the process exits. A chunk-based read surfaces whatever bytes are
# actually available the moment they arrive, matching how a real terminal behaves. EOF is a
# 0-length read (not a null result the way ReadLineAsync signals it). See
# docs/plan-cli-interactive-verification.md Finding 9 for the full empirical trace.
#
# Full rationale + citations: docs/plan-cli-interactive-verification.md Findings 5b/6/7/8/9.
#
# This is the canonical source for the HP_FAILFAST_PROBE base64 payload embedded in
# run_setup.bat. After editing, re-encode and paste it into the `set "HP_FAILFAST_PROBE=..."`
# line; tests/test_failfast_probe.py asserts the embedded payload matches this file (with
# CRLF/LF normalized, per the .ps1 PayloadSync convention -- see
# docs/agent-lessons-learned.md "Embedded Helper Update Workflow").
$exe = $env:HP_PROBE_EXE
$rawArgs = $env:HP_PROBE_ARGS
$workDir = $env:HP_PROBE_CWD
$probeMs = [int]$env:HP_FAILFAST_PROBE_MS
$outPath = $env:HP_PROBE_OUT
if (-not $outPath) { $outPath = '~run.out.txt' }
$errPath = $env:HP_PROBE_ERR
if (-not $errPath) { $errPath = '~run.err.txt' }
$resultPath = $env:HP_PROBE_RESULT
if (-not $resultPath) { $resultPath = '~probe_result.txt' }

$si = New-Object System.Diagnostics.ProcessStartInfo
$si.FileName = $exe
if ($rawArgs) { $si.Arguments = $rawArgs }
$si.WorkingDirectory = $workDir
$si.UseShellExecute = $false
$si.RedirectStandardOutput = $true
$si.RedirectStandardError = $true
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $si
$p.Start() | Out-Null
Write-Host "[INFO] Process ID $($p.Id). If it seems stuck: Task Manager > Details tab > find this PID > End Task (this window stays open)."

$outBuf = New-Object System.Text.StringBuilder
$errBuf = New-Object System.Text.StringBuilder
$outChunkBuf = New-Object char[] 4096
$errChunkBuf = New-Object char[] 4096
$outTask = $p.StandardOutput.ReadAsync($outChunkBuf, 0, $outChunkBuf.Length)
$errTask = $p.StandardError.ReadAsync($errChunkBuf, 0, $errChunkBuf.Length)
$outDone = $false
$errDone = $false

$sw = [System.Diagnostics.Stopwatch]::StartNew()
$exceeded = 0
while ((-not $p.HasExited) -or (-not $outDone) -or (-not $errDone)) {
    if ((-not $outDone) -and $outTask.IsCompleted) {
        $n = $outTask.Result
        if ($n -eq 0) {
            $outDone = $true
        } else {
            $chunk = [string]::new($outChunkBuf, 0, $n)
            [Console]::Out.Write($chunk)
            $null = $outBuf.Append($chunk)
            $outTask = $p.StandardOutput.ReadAsync($outChunkBuf, 0, $outChunkBuf.Length)
        }
    }
    if ((-not $errDone) -and $errTask.IsCompleted) {
        $n = $errTask.Result
        if ($n -eq 0) {
            $errDone = $true
        } else {
            $chunk = [string]::new($errChunkBuf, 0, $n)
            [Console]::Error.Write($chunk)
            $null = $errBuf.Append($chunk)
            $errTask = $p.StandardError.ReadAsync($errChunkBuf, 0, $errChunkBuf.Length)
        }
    }
    if ((-not $exceeded) -and ($sw.ElapsedMilliseconds -ge $probeMs)) {
        $exceeded = 1
    }
    Start-Sleep -Milliseconds 20
}
$p.WaitForExit()

$outBuf.ToString() | Set-Content -Path $outPath -Encoding ASCII
$errBuf.ToString() | Set-Content -Path $errPath -Encoding ASCII
"$exceeded|$($p.ExitCode)" | Set-Content -Path $resultPath -Encoding ASCII
